package com.gitee.wsl.time.timer

import com.gitee.wsl.time.nextMatch
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch
import kotlin.time.Clock
import kotlin.time.Instant
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.Month
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import kotlin.jvm.JvmInline
import kotlin.time.Duration.Companion.minutes
import kotlin.time.ExperimentalTime

/**
 * A [Pulse] is a cadence which informs consumers when to execute work by calling [Pulse.beat].
 */
@OptIn(ExperimentalTime::class)
@JvmInline
value class Pulse internal constructor(private val flow: Flow<Pair<Instant, Instant>>) {

    /**
     * Returns a pulse that beats [count] times.
     *
     * @throws IllegalArgumentException is count is not positive.
     */
     fun take(count: Int): Pulse = Pulse(flow.take(count))

    /**
     * Returns a pulse that beats while [predicate] is satisfied.
     */
     fun takeWhile(predicate: (Instant, Instant) -> Boolean): Pulse = Pulse(
        flow.takeWhile { (scheduled, occurred) -> predicate(scheduled, occurred) }
    )

    /**
     * Invoke [action] every time this Pulse is set to execute. [Action][action] provides two [instants][Instant]
     * denoting when the pulse was scheduled to occur, and when it actually occurred.
     *
     * This operator will execute [action] according to which [mode] is specified.
     */
     suspend fun beat(
        mode: RecurringJobMode = RecurringJobMode.CancellingSequential,
        action: suspend (scheduled: Instant, occurred: Instant) -> Unit,
    ): Unit = when (mode) {
        RecurringJobMode.CancellingSequential -> flow.collectLatest { (scheduled, occurred) ->
            action(scheduled, occurred)
        }
        RecurringJobMode.Concurrent -> coroutineScope {
            flow.collect { (scheduled, occurred) ->
                launch { action(scheduled, occurred) }
            }
        }
    }

    companion object{

        /**
         * Schedule a [Pulse] whose beats occur [atSecond], [atMinute], [atHour], [onDayOfMonth], and [inMonth] for a specific
         * [timeZone].
         *
         * Null values for any parameter indicate that a beat can occur on _any_ second, minute, hour, etc. For example,
         * scheduling a pulse on the fifth second of every minute would look like the following:
         *
         * ```kt
         * clock.schedulePulse(atSecond = 5)
         * ```
         *
         * While scheduling a pulse to occur on the third of every month at 12:30 would look like the following:
         *
         * ```kt
         * clock.schedulePulse(atSecond = 0, atMinute = 30, atHour = 12, onDayOfMonth = 3)
         * ```
         *
         * Scheduling pulses is done in local time and is therefore subject to daylight savings time adjustments. Local time
         * conversion is sometimes ambiguous, and therefore it's recommended to schedule pulses in a fixed UTC offset timezone.
         * See [LocalDateTime.toInstant] for more details.
         *
         * @param atSecond The second of a minute to pulse at. Null matches the whole valid range, 0..59.
         * @param atMinute The minute of an hour to pulse at. Null matches the whole valid range, 0..59.
         * @param atHour The hour of a day to pulse at. Null matches the whole valid range, 0..23.
         * @param onDayOfMonth The day of a month to pulse at. Null matches the whole valid range, 0..31.
         * @param inMonth The month of a year to pulse at. Null matches the whole valid range, January..December.
         * @throws [IllegalArgumentException] if any parameter is out of the above range.
         */
        fun schedulePulse(
            timeZone: TimeZone = TimeZone.UTC,
            atSecond: Int? = null,
            atMinute: Int? = null,
            atHour: Int? = null,
            onDayOfMonth: Int? = null,
            inMonth: Month? = null,
        ): Pulse {
            val flow = flow{
                var lastPulse: LocalDateTime = Clock.System.now().toLocalDateTime(timeZone)
                while (true) {
                    val nextPulse = lastPulse.nextMatch(
                        atSeconds = atSecond?.let { it..it } ?: 0..59,
                        atMinutes = atMinute?.let { it..it } ?: 0..59,
                        atHours = atHour?.let { it..it } ?: 0..23,
                        onDaysOfMonth = onDayOfMonth?.let { it..it } ?: 1..31,
                        inMonths = inMonth?.let { it..it } ?: Month.JANUARY..Month.DECEMBER,
                    )
                    delayUntil(nextPulse, timeZone)
                    emit(Pair(nextPulse.toInstant(timeZone), Clock.System.now()))
                    lastPulse = nextPulse
                }
            }
            return Pulse(flow)
        }

        /**
         * Suspend the coroutine until [instant] without blocking the thread. If [Clock.now] is greater than [instant], this
         * function returns immediately.
         *
         * Coroutines delayed for more than one minute will occassionally be resumed to account for clock drift.
         */
         suspend fun delayUntil(instant: Instant) {
            var now = Clock.System.now()
            while (now < instant) {
                val durationUntilInstant = instant - now
                val delayIncrement = minOf(durationUntilInstant, 1.minutes)
                delay(delayIncrement)
                now = Clock.System.now()
            }
        }

        /**
         * Suspend the coroutine until [dateTime] in [timeZone] without blocking the thread. If [Clock.now] is greater than
         * the [Instant] associated with [dateTime] and [timeZone], this function returns immediately.
         *
         * Coroutines delayed for more than one minute will occassionally be resumed to account for clock drift.
         */
        suspend fun delayUntil(dateTime: LocalDateTime, timeZone: TimeZone) {
            delayUntil(instant = dateTime.toInstant(timeZone))
        }
    }
}

 enum class RecurringJobMode {
    /**
     * Schedules recurring jobs concurrently. That is if job `n` is still active when job
     * `n + 1` should begin, then job `n` continues to run concurrently alongside job `n + 1`.
     */
    Concurrent,
    /**
     * Schedules recurring jobs sequentially by cancelling an unfinished job if a new one
     * should begin executing.
     */
    CancellingSequential,
}